//index.hpp用于建立我们的索引
//正排索引：就是从文档ID找到文档内容(文档内的关键字)
//倒排索引：根据文档内容，分词，整理不重复的各个关键字，对应联系到文档ID的方案

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <unordered_map>
#include <mutex>
#include "util.hpp"
#include "log.hpp"

//用户输入：小米 -> 倒排索引中查找 -> 提取出文档ID(1,2) -> 根据正排索引 -> 找到文档的内容 ->
//title+conent（desc）+url 文档结果进行摘要->构建响应结果

//搜索：北京  ->倒排索引  北京景点(id=1 w=3) 北京小吃(id=5 w=7) 去北京(id=12 w=8) 北京欢迎你(id=3 w=4)
//获取文档id(12,5,3,1[根据w大小]) ->正排索引 获取id=12,5,3,1的内容 ->进行摘要
//输出：去北京 北京小吃 北京欢迎你 北京景点

namespace my_Index
{
    typedef struct DocInfo
    {
        std::string title; //文件的标题 
        std::string content; //文件的内容
        std::string url; //文件的url
        int doc_id; //文档的id
    }DocInfo_t;

    //这是单个关键字和文档id之间的权重
    //倒排元素结构体，用于我们在倒排索引中找到文档信息
    typedef struct InvertedElem
    {  
        std::string word; //关键字
        int weight; //权重
        int doc_id; //文档id
    }InvertedElem_t;

    //因为倒排索引一定是一个关键字和至少一个InvertedElem_t文件对应
    //搜索：北京  结果：北京景点 北京小吃 去北京 北京欢迎你
    //倒排拉链
    typedef std::vector<InvertedElem_t> InvertedList;

    class Index
    {
        private:
        //这个是正排索引
        //正排索引，数组的下表就是文档的id
        std::vector<DocInfo_t> Foward_Index;
        
        //这个是倒排索引
        //我们要有输入的关键字和倒排拉链 (多个倒排文档{关键字 权重 文档id}) 的映射关系
        std::unordered_map<std::string,InvertedList> Inverted_Index; 

    private:
        //避免index创建多份影响效率我们使用单例模式
        //私有化index的构造函数，拷贝构造函数和赋值运算符重载
        //提供一个供外部使用的接口
        Index(){}; //为了构造对象我们要有函数体
        Index(const Index&)=delete;
        Index& operator=(const Index&)=delete;

        static Index* instance;
        static std::mutex mtx;//对构造进行加锁，防止创建两次
    public:
        //Index索引的构造和析构函数
        //Index();
        ~Index();
    
    public:
        //获取单例函数
        static Index* GetInstance()
        {
            //挡住大部分加锁的申请，提高效率
            if(nullptr==instance)
            {
                mtx.lock();//加锁

                //如果没有创建我们的单例就创建一个
                if(nullptr==instance)
                {
                    instance=new Index();
                }

                mtx.unlock();//解锁
            }
            return instance;
        }

        //根据doc_id找到文档内容，正排索引
        DocInfo_t *GetForwardIndex(int doc_id)
        {
            //返回正排索引的文件内容
            if(doc_id>Foward_Index.size())
            {
                std::cerr<<"doc_id out range,error!"<<std::endl;
                return nullptr;
            }
            return &Foward_Index[doc_id];
        }

        //根据关键字找到倒排拉链
        InvertedList *GetInvertedList(const std::string &word)
        {
            //返回倒排索引的文件内容，因为通过word找到的文档可能有多个，所以我们返回倒排拉链
            auto iter =Inverted_Index.find(word);
            if(iter==Inverted_Index.end())
            {
                std::cerr<<word<<" have no invertedlist!"<<std::endl;
                return nullptr;
            }
            return &iter->second;
        }

        //构建正排和倒排索引，这里处理parser处理完的文件 
        //parser处理完的文件放在了 data/raw_html/raw.txt
        bool BuildIndex(const std::string &html_Files)
        {
            //打开并且读取raw.txt的文件
            std::ifstream in(html_Files,std::ios::in|std::ios::binary);
            //这里判断文件是否打开成功，失败直接返回错误
            if(!in.is_open())
            {
                std::cerr<<html_Files<<" open error!"<<std::endl;
                return false;
            }

            //我们把打开的文件中的信息输入到line
            //我们在raw.txt中读取一条文件，就建立一个正排和倒排索引
            std::string line;
            int count=0; //进行测试打印
            while(std::getline(in,line))
            {
                //构建正排索引
                DocInfo_t *doc=BuildForwardIndex(line);
                if(nullptr==doc)
                {
                    std::cerr<<"build "<<line<<" error"<<std::endl;
                    continue;
                }
                
                //构建倒排索引
                BuildInvertedIndex(*doc);
                count++;
                if(count%100==0) //打印建立文档的进度
                {
                    //std::cout<<"当前已经建立的的索引文档："<<count<<std::endl;
                    LOG(NORMAL, "当前的已经建立的索引文档: " + std::to_string(count));
                }
            }
            return true;
        }

    private:
        //构建正排索引的实现
        //我们先获取一个进行过分割处理过的html文件，然后将html进行切分字符串暂存到result，
        //最后result根据Fowrd_Index的大小储存在Foward_Index中
        DocInfo_t *BuildForwardIndex(const std::string &line)
        {
            //创建一个存储line切分的临时数组
            std::vector<std::string> result;

            //解析字符串进行切分，注意分隔符是\3
            std::string sep="\3"; //行内分割符
            my_util::StringUtil::SplitString(line,&result,sep);
            if(result.size()!=3) //如果切分的文件result不满足我们的要求，直接返回错误
            {
                return nullptr;
            }

            //我们将切分后的字符串填充到DocInfo_t
            DocInfo_t doc;
            doc.title=result[0];
            doc.content=result[1];
            doc.url=result[2];
            doc.doc_id=Foward_Index.size(); //文件的doc_id我们就使用正排索引的下标表示

            //最后将我们填充好的doc文件存入正排索引，并且返回
            Foward_Index.push_back(std::move(doc));
            return &Foward_Index.back();
        }

        //构建倒排索引的实现
        bool BuildInvertedIndex(const DocInfo_t &doc)
        {
            //1.进行分词
            //创建一个临时的词频统计结构体，用来统计标题和内容的词频
            struct word_cnt
            {
                int title_cnt; //统计分词在标题中出现的次数
                int content_cnt; //统计分词在内容中出现的次数

                word_cnt():title_cnt(0),content_cnt(0){} //将结构体成员初始化为0
            };

            //创建一个暂存分词词频的表
            //word_map表示分词在doc标题和文章中出现的次数是多少
            std::unordered_map<std::string,word_cnt> word_map;

            //对标题进行分词，并将分好的词放入title_words中
            std::vector<std::string> title_words;
            my_util::JiebaUtil::CutString(doc.title,&title_words);

            //对内容进行分词，并将分好的词放入content_words中
            std::vector<std::string> content_words;
            my_util::JiebaUtil::CutString(doc.content,&content_words);

            //2.统计词频
            //统计title_words中分好词的s的出现次数，如果是第一次出现在word_map就新加入
            for(auto &s:title_words)
            {
                boost::to_lower(s);
                word_map[s].title_cnt++;
            }

            //同上，统计分词s在文章内容中出现的次数
            for(auto &s:content_words)
            {
                boost::to_lower(s);
                word_map[s].content_cnt++;
            }

            //3.自定义相关性
            //这里我们直接将出现在标题中的s的权重设置的大一点
#define X 10
#define Y 1
            //我们将暂存分词的表中的数据存放到我们的倒排链接中
            for(auto &word_pair:word_map)
            {
                //创建单个倒排的元素结构体，用于构建我们的倒排拉链
                InvertedElem_t item; 
             
                //我们将doc中的doc_id和经过处理过后的存在title和content中的分词信息存入我们的item中
                item.doc_id=doc.doc_id;
                item.word=word_pair.first;
                item.weight=word_pair.second.title_cnt*X+word_pair.second.content_cnt*Y; //自定义我们的相关性
                
                //将我们处理好的item插入到倒排索引对应的倒排拉链中
                //如果倒排索引中没有这个词，就新建一个倒排拉链，用于存储该词对应的信息
                
                //inverted_list表示对分词在倒排索引中的位置的引用
                //倒排索引中存储的是倒排拉链，将item插入和倒排索引string相同的拉链中即可

                //文章一 title: I love you!  content:you know many things... url:XXX id=1
                //文章二 title: How to love? content:I know,I love...  ur2:YYY  id=2(正排索引)
                //....
                //
                // 分词结果  I / love / you / love you / konw
                // 倒排索引  口--->口--->口---->口------->口
                // 倒排拉链   |     ...      ...          |
                //    doc_id:1  word:"I"  weight:10   
                //           |                          ...
                //    doc_id:2  word:"I"  weight:2
                //           |                           |
                //        ......                       ......
                InvertedList &inverted_list=Inverted_Index[word_pair.first];
                inverted_list.push_back(std::move(item));
            }

            return true;
        }
    };
    //初始化我们的单例对象
    Index* Index::instance=nullptr;
    std::mutex Index::mtx;
}